package com.ar4j.spring;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import javax.sql.DataSource;

import org.springframework.beans.BeanMetadataAttribute;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.AutowireCandidateQualifier;
import org.springframework.transaction.PlatformTransactionManager;

import com.ar4j.ActiveRecord;
import com.ar4j.IActiveRecordFactory;
import com.ar4j.sql.INamedQuerySource;

/**
 * A factory class for building adapters.
 */
public class SpringActiveRecordAdapterFactory {
  private static final String DEFAULT_QUALIFIER_VALUE = "_default";
  
  private static Map<String, SpringActiveRecordAdapter<?>> adapterCache = Collections.synchronizedMap(new HashMap<String, SpringActiveRecordAdapter<?>>());
  private static ReentrantLock adapterCacheLock = new ReentrantLock();
  
  private static Map<String, IActiveRecordFactory> factoryCache = Collections.synchronizedMap(new HashMap<String, IActiveRecordFactory>());
  private static ReentrantLock factoryCacheLock = new ReentrantLock();

  /**
   * Returns a cached or a newly constructed spring active record adapter for the given base class and qualifier annotation. 
   * The qualifier annotation (if provided) will be used to located qualified beans to construct an active record factory 
   * (i.e. DataSource, PlatformTransactionManager, etc.).
   *  
   */
  @SuppressWarnings("unchecked")
  public static SpringActiveRecordAdapter<?> getSpringActiveRecordAdapter(ConfigurableListableBeanFactory beanFactory, Class<?> baseClass, Qualifier qualifierAnnotation) {
    
    // validate base class
    if(!ActiveRecord.class.isAssignableFrom(baseClass)) {
      throw new IllegalArgumentException("Base class must be an active record implementor: " + baseClass);
    }
    
    // try to find a qualifier on the base class if none was provided
    if(qualifierAnnotation == null) {
      qualifierAnnotation = baseClass.getAnnotation(Qualifier.class);
    }
    
    // use the default if no qualifier is found
    String qualifier = (qualifierAnnotation != null ? qualifierAnnotation.value() : DEFAULT_QUALIFIER_VALUE);
    
    // get actual adapter
    String adapterKey = qualifier + "|" + baseClass.getName();
    SpringActiveRecordAdapter<?> out = adapterCache.get(adapterKey);
    if(out == null) {
      adapterCacheLock.lock();
      try {
        out = adapterCache.get(baseClass);
        if(out == null) {
          IActiveRecordFactory factory = getFactoryForQualifier(beanFactory, qualifier);
          out = new SpringActiveRecordAdapter(baseClass, factory);
          adapterCache.put(adapterKey, out);
        }
      } finally {
        adapterCacheLock.unlock();
      }
    }
    
    return out;
  }
  
  /**
   * Creates a ContextSettingActiveRecordFactory instance with its values retrieved from 
   * the application context using the given qualifier, or default values retrieved (non qualified) 
   * if the default qualifier was given.
   */
  private static IActiveRecordFactory getFactoryForQualifier(ConfigurableListableBeanFactory beanFactory, String qualifier) {
    IActiveRecordFactory out = factoryCache.get(qualifier);
    if(out == null) {
      factoryCacheLock.lock();
      try {
        DataSource dataSource = getQualifiedBean(beanFactory, true, DataSource.class, qualifier);
        PlatformTransactionManager txManager = getQualifiedBean(beanFactory, false, PlatformTransactionManager.class, qualifier);
        INamedQuerySource namedQuerySource = getQualifiedBean(beanFactory, false, INamedQuerySource.class, qualifier);
        
        out = new ContextSettingActiveRecordFactory(null, dataSource, txManager, namedQuerySource);
        factoryCache.put(qualifier, out);
      } finally {
        factoryCacheLock.unlock();
      }
    }
    
    return out;
  }
  
  /**
   * Retrieves a qualified bean from the context, if the qualifier is not the default one.
   * If a single value (qualified or not is available) it is returned. For unqualified (or
   * default qualified) retrievals the primary autowire candidate is returned.
   */
  @SuppressWarnings("unchecked")
  private static <E> E getQualifiedBean(ConfigurableListableBeanFactory beanFactory, boolean required, Class<E> beanClass, String qualifier) {
    E out = null;
    
    // get the only possible bean if we are using the default qualifier
    if(DEFAULT_QUALIFIER_VALUE.equals(qualifier)) {
      
      // find the primary (or only) bean definition for the given type
      Map<String, E> beans = beanFactory.getBeansOfType(beanClass);
      E primary = null;
      for(Map.Entry<String, E> entry : beans.entrySet()) {
        BeanDefinition definition = beanFactory.getBeanDefinition(entry.getKey());
        if(beans.size() == 1 || definition.isPrimary()) {
          primary = entry.getValue();
        }
      }
      
      // make sure we found the primary if we found any beans
      if(beans.size() >= 1 && primary == null) {
        throw new IllegalArgumentException("Found more than one default qualified bean of type: " + beanClass + ", got: " + beans);
      } else {
        out = primary;
      }
    } 
    
    // or try to look it up by qualifier name
    else {
      String[] beanNames = beanFactory.getBeanNamesForType(beanClass);
      E unqualifiedCandidate = null;
      for(String beanName : beanNames) {
        AbstractBeanDefinition definition = (AbstractBeanDefinition) beanFactory.getBeanDefinition(beanName);
        
        // if we are the only bean (or primary) , we become the unqualified candidate
        if(beanNames.length == 1 || definition.isPrimary()) {
          unqualifiedCandidate = (E) beanFactory.getBean(beanName);
        }
        
        // go through the definition and see if we have this qualifier
        for(AutowireCandidateQualifier autowireQualifier : definition.getQualifiers()) {
          BeanMetadataAttribute attr = autowireQualifier.getMetadataAttribute("value");
          if(qualifier.equals(attr.getValue())) {
            if(out != null) {
              throw new IllegalArgumentException("More than one qualified bean found of type: " + beanClass + ", qualifier: " + qualifier);
            } else {
              out = (E) beanFactory.getBean(beanName);
            }
          }
        }
      }
      
      // if we didn't find by qualifier but we have an unqualified candidate, use it
      if(out == null && unqualifiedCandidate != null) {
        out = unqualifiedCandidate;
      }
    }
    
    // make sure we got one if it was required
    if(out == null && required) {
      throw new IllegalArgumentException("Could not find qualified bean of type: " + beanClass + ", qualifier: " + qualifier);
    }
    
    return out;
  }
}
